/*
 * Copyright 2013 JROSE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jrose.core.jdbc.persistence.query;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.jrose.core.util.StringFormatter;
import org.springframework.util.Assert;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
public class Criteria implements Serializable {
    private static final long serialVersionUID = -2724166952869268367L;

    // If the 'value' set to 'NO_VALUE', means current criteria has no value available
    private static final Object NO_VALUE = ObjectUtils.NULL;

    @Getter private String field = null;
    @Getter private Object value = null;
    private String operator = null;

    @Getter private String joiner = null;
    @Getter private final List<Criteria> chain = new LinkedList<>();

    boolean valueAvailable() {
        return value != NO_VALUE;
    }

    // COMPOSITE

    public static Criteria and(Criteria... criterias) {
        Assert.notNull(criterias);

        Criteria criteria = new Criteria();
        criteria.joiner = "$and";
        criteria.chain.addAll(Arrays.asList(criterias));
        return criteria;
    }

    public static Criteria or(Criteria... criterias) {
        Assert.notNull(criterias);

        Criteria criteria = new Criteria();
        criteria.joiner = "$or";
        criteria.chain.addAll(Arrays.asList(criterias));
        return criteria;
    }

    // SINGLE

    public static Criteria where(String field) {
        Assert.notNull(field);

        Criteria criteria = new Criteria();
        criteria.field = field;
        return criteria;
    }

    public Criteria is(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$eq";
        this.value = value;
        return this;
    }

    public Criteria ne(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$ne";
        this.value = value;
        return this;
    }

    public Criteria lt(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$lt";
        this.value = value;
        return this;
    }

    public Criteria le(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$le";
        this.value = value;
        return this;
    }

    public Criteria gt(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$gt";
        this.value = value;
        return this;
    }

    public Criteria ge(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$ge";
        this.value = value;
        return this;
    }

    public Criteria in(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);
        Assert.notNull(value);
        Assert.isTrue(value instanceof Collection);

        this.operator = "$in";
        this.value = value;
        return this;
    }

    public Criteria notIn(Object value) {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);
        Assert.notNull(value);
        Assert.isTrue(value instanceof Collection);

        this.operator = "$notin";
        this.value = value;
        return this;
    }

    public Criteria isNull() {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$isnull";
        this.value = NO_VALUE;
        return this;
    }

    public Criteria isNotNull() {
        Assert.isTrue(field != null);
        Assert.isTrue(operator == null);

        this.operator = "$isnotnull";
        this.value = NO_VALUE;
        return this;
    }

    public String generateSQL(String columnName, String parameterName) {
        Assert.notNull(columnName);
        if (valueAvailable()) Assert.notNull(parameterName);

        switch (operator) {
            case "$eq": {
                String sql = "{}=:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$ne": {
                String sql = "{}<>:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$lt": {
                String sql = "{}<:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$le": {
                String sql = "{}<=:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$gt": {
                String sql = "{}>:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$ge": {
                String sql = "{}>=:{}";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$in": {
                String sql = "{} IN (:{})";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$notin": {
                String sql = "{} NOT IN (:{})";
                return StringFormatter.formatMessage(sql, columnName, parameterName);
            }
            case "$isnull": {
                String sql = "ISNULL({})";
                return StringFormatter.formatMessage(sql, columnName);
            }
            case "$isnotnull": {
                String sql = "{} IS NOT NULL";
                return StringFormatter.formatMessage(sql, columnName);
            }
            default: {
                throw new UnsupportedOperationException();
            }
        }
    }
}
